﻿using System;
using System.Collections.Generic;
using System.ComponentModel;

namespace MvvmFx.Common.ViewModels.Behaviors.Observable
{
    /// <summary>
    /// ViewModel class that inherits from <see cref="ViewModelBase"/> and
    /// provides additional functionality for observing property changes on
    /// the ViewModel on a per-property basis.
    /// </summary>
    /// <example>
    /// To add a handler to changes in a property:
    /// <code>
    ///   documentsPage.AddHandler("IsLoaded",
    ///                            delegate { addDocuments.RaiseCanExecuteChanged(); });
    /// </code>
    /// </example>
    /// <remarks>
    /// Adding a handler to a ViewModel object creates a strong object reference to it. 
    /// This means the object will not be garbage collected as long as the referencing 
    /// object has a handler to it. This can lead to memory leaks if the handlers are 
    /// not explicitly removed. This can be done in three ways:
    /// <example>
    /// Call the RemoveHandler method when the referencing object does not need to receive
    /// property change notifications anymore:
    /// <code>
    /// documentsPage.RemoveHandler("IsLoaded",
    ///                            OnDocumentsPageIsLoadedChanged);
    /// </code>
    /// </example>
    /// <example>
    /// Call the protected RemoveHandlers method when the referenced object should not notify its
    /// observers anymore:
    /// <code>
    /// this.RemoveHandlers()
    /// </code>
    /// </example>
    /// <example>
    /// Call the Dispose method of the ObservableViewModel before the object goes out of scope:
    /// <code>
    /// documentsPage.Dispose();
    /// </code>
    /// This calls the RemoveHandlers method internally.
    /// </example>
    /// </remarks>
    public class ObservableViewModel : ViewModelBase,
                                       IDisposable
    {
        /// <summary>
        /// The dictionary of property changed handlers
        /// </summary>
        private readonly Dictionary<string, Delegate> propertyChangedHandlersDictionary = new Dictionary<string, Delegate>();

        /// <summary>
        /// Overrides the OnPropertyChanged method of ViewModelBase
        /// and invokes any added handlers
        /// </summary>
        /// <param name="propertyName">The name of the property that has changed</param>
        protected override void OnPropertyChanged(string propertyName)
        {
            base.OnPropertyChanged(propertyName);

            Delegate handler;
            if (this.propertyChangedHandlersDictionary.TryGetValue(propertyName, out handler))
            {
                var args = new PropertyChangedEventArgs(propertyName);
                handler.DynamicInvoke(this, args);
            }
        }

        #region Property Change Notification Handlers

        /// <summary>
        /// Add a property changed event handler for the specfiied propertyName
        /// </summary>
        /// <param name="propertyName">The name of the property to attach the handler to.</param>
        /// <param name="handler">The handler to attach.</param>
        public void AddHandler(string propertyName, EventHandler handler)
        {
            Delegate handlers;
            if (this.propertyChangedHandlersDictionary.TryGetValue(propertyName, out handlers))
            {
                handlers = Delegate.Combine(handlers, handler);

                this.propertyChangedHandlersDictionary[propertyName] = handlers;
            }
            else
            {
                this.propertyChangedHandlersDictionary.Add(propertyName, handler);
            }
        }

        /// <summary>
        /// Remove a property changed event handler for the specfiied propertyName
        /// </summary>
        /// <param name="propertyName">The name of the property to remove the handlers for.</param>
        /// <param name="handler">The handler to remove.</param>
        public void RemoveHandler(string propertyName, EventHandler handler)
        {
            Delegate handlers;
            if (this.propertyChangedHandlersDictionary.TryGetValue(propertyName, out handlers))
            {
                handlers = Delegate.Remove(handlers, handler);

                if (handlers == null)
                {
                    this.propertyChangedHandlersDictionary.Remove(propertyName);
                }
                else
                {
                    this.propertyChangedHandlersDictionary[propertyName] = handlers;
                }
            }
        }

        /// <summary>
        /// Remove all the handlers to clean up the object references.
        /// Called by the Dispose method.
        /// </summary>
        protected virtual void RemoveHandlers()
        {
            foreach (KeyValuePair<string, Delegate> handlers in this.propertyChangedHandlersDictionary)
            {
                var delegates = handlers.Value.GetInvocationList();
                foreach (var del in delegates)
                {
                    Delegate.Remove(handlers.Value, del);
                }
            }

            this.propertyChangedHandlersDictionary.Clear();
        }

        #endregion

        #region IDisposable Members

        private bool disposed;

        /// <summary>
        /// The Dispose method
        /// </summary>
        public void Dispose()
        {
            Dispose(true);

            // Use SupressFinalize in case a subclass
            // of this type implements a finalizer.
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// The protected virtual dispose that removes the handlers
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            // If you need thread safety, use a lock around these 
            // operations, as well as in your methods that use the resource.
            if (!disposed)
            {
                if (disposing)
                {
                    RemoveHandlers();
                }

                // Indicate that the instance has been disposed.
                disposed = true;
            }
        }

        #endregion
    }
}